package de.hpi.generator.generator.distributors;

import de.hpi.generator.config.DistributionModel;
import de.hpi.generator.config.DistributionModelPOISSON;
import de.hpi.generator.generator.storages.ValueSeriaInfo;
import java.math.BigDecimal;

public class PoissonDistributor extends DataDistributor {

    private double lambda = 0;
    private double aPoissonCoefficient = 0.0;
    private int aEffectiveRangeStart = 0;

    @Override
    public String getName() {
        return "POISSON";
    }

    public double getPoissonCoefficient() {
        return aPoissonCoefficient;
    }

    public int getEffectiveRangeStart() {
        return aEffectiveRangeStart;
    }

    public double getLambda() {
        return lambda;
    }

    public PoissonDistributor() throws Exception {
        super();
        initClass(1);
    }

    public PoissonDistributor(double aLambda) throws Exception {
        super();
        initClass(aLambda);
    }

    /**
     * setup object with specialized params
     * 
     * @param aSpecializedDistributionModel
     *            specialized params
     */
    @Override
    public void setupObjectWithSpecializedParams(DistributionModel aSpecializedDistributionModel) throws Exception {
        super.setupObjectWithSpecializedParams(aSpecializedDistributionModel);
        // convert common type of aSpecializedDistributionModel object to
        // specialized type
        DistributionModelPOISSON aDistributionModelPOISSON = (DistributionModelPOISSON) aSpecializedDistributionModel;

        // setup distributor parameters
        this.initClass(aDistributionModelPOISSON.lambda);

    }

    private void initClass(double lambda) throws Exception {
        validateLambda(lambda);
        this.lambda = lambda;
        this.aPoissonCoefficient = Math.exp(-lambda);
    }

    private void validateLambda(double lambda) throws Exception {
        if (lambda < 0 || lambda > 700) {
            throw new Exception(String.format(
                    "The lambda [%g] must be between 0 and 700",
                    lambda));
        }
    }

    @Override
    public void setupRange(int aValuesCount) throws Exception {
        super.setupRange(aValuesCount);
        this.aEffectiveRangeStart = findEffectiveRangeStart();
        this.random.calibrate(this.aEffectiveRangeStart, findMaxDeviationOfExpectedValueAndAllValuesCount());
    }

    /**
     * generates random positive integer
     * 
     * @return random positive integer
     */
    @Override
    public ValueSeriaInfo generateNextRandomValueSeriaInfo() throws Exception {
        int aRandomInteger = random.getRandomValue();
        int aRandomIntegerCount = this.getCountOfValue(aRandomInteger);

        return new ValueSeriaInfo(aRandomInteger, aRandomIntegerCount);
    }

    @Override
    public int getCountOfValue(int aValue) throws Exception {
        if(aValue < 0) return 0;
        return (int) (poisson(aValue).doubleValue() * aNumberOfValuesToGenerate);
    }

    private BigDecimal poisson(int aValue) throws Exception {
        BigDecimal aLambdaCounter = BigDecimal.valueOf(lambda).pow(aValue);
        BigDecimal aFactorial = factorial(aValue);
        BigDecimal aFraction = aLambdaCounter.divide(aFactorial, 6, BigDecimal.ROUND_HALF_UP);
        return aFraction.multiply(BigDecimal.valueOf(aPoissonCoefficient));
    }

    private int findMaxDeviationOfExpectedValueAndAllValuesCount() throws Exception {
        int deviation = 0;
        this.aAllValuesCount = 0;
        for (deviation = this.aEffectiveRangeStart;; deviation++) {
            int aCurrentCountOfValue = this.getCountOfValue(deviation);

            if (aCurrentCountOfValue == 0 && deviation > lambda) {
                break;
            }

            this.aAllValuesCount += aCurrentCountOfValue;
        }

        return deviation;
    }

    private int findEffectiveRangeStart() throws Exception {
        int result = 0;
        for (result = 0; this.getCountOfValue(result) == 0 && result < lambda; result++) {
        }

        if (result == lambda) {
            throw new Exception(String.format(
                    "The number of values to generate [%d] is too small for this lambda [%g]",
                    aNumberOfValuesToGenerate,
                    lambda));
        }
        return result;
    }

    private BigDecimal factorial(int aValue) throws Exception {

        BigDecimal factorial = new BigDecimal("1");
        for (int aCurrentNumber = 1; aCurrentNumber <= aValue; aCurrentNumber++) {
            factorial = factorial.multiply(new BigDecimal(String.valueOf(aCurrentNumber)));
        }
        return factorial;
    }
}
